1 /* 2 * Hunt - a framework for web and console application based on Collie using Dlang development 3 * 4 * Copyright (C) 2015-2017 Shanghai Putao Technology Co., Ltd 5 * 6 * Developer: HuntLabs 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.routing.router; 13 14 import hunt.routing.define; 15 import hunt.routing.routegroup; 16 import hunt.routing.route; 17 import hunt.routing.config; 18 19 import hunt.application.controller; 20 21 import std.file; 22 import std.path; 23 import std.array; 24 25 class Router 26 { 27 public 28 { 29 this() 30 { 31 this._defaultGroup = new RouteGroup(DEFAULT_ROUTE_GROUP); 32 } 33 34 void setConfigPath(string path) 35 { 36 // supplemental slash 37 this._configPath = (path[$-1] == '/') ? path : path ~ "/"; 38 } 39 40 string createUrl(string mca, string[string] params, string group = DEFAULT_ROUTE_GROUP) 41 { 42 // find Route 43 RouteGroup routeGroup = this.getGroup(group); 44 45 if (routeGroup is null) 46 { 47 return "#"; 48 } 49 50 Route route = routeGroup.getRoute("",mca); 51 52 if (route is null) 53 { 54 return "#"; 55 } 56 57 string url; 58 59 if (route.getRegular() == true) 60 { 61 if (params.length == 0) 62 { 63 logWarningf("this route need params (%s).", mca); 64 65 return "#"; 66 } 67 68 if (route.getParamKeys().length > 0) 69 { 70 url = route.getUrlTemplate(); 71 72 import std.array : replaceFirst; 73 74 foreach (i, key; route.getParamKeys()) 75 { 76 string value = params.get(key, null); 77 78 if (value is null) 79 { 80 logWarningf("this route template need param (%s).", key); 81 82 return "#"; 83 } 84 85 params.remove(key); 86 87 url = url.replaceFirst("{" ~ key ~ "}", value); 88 } 89 } 90 } 91 else 92 { 93 url = route.getPattern(); 94 } 95 96 return url ~ (params.length > 0 ? ("?" ~ buildUriQueryString(params)) : ""); 97 } 98 99 string buildUriQueryString(string[string] params) 100 { 101 if (params.length == 0) 102 { 103 return ""; 104 } 105 106 string uriQueryString; 107 108 foreach (k, v; params) 109 { 110 uriQueryString ~= (uriQueryString ? "&" : "") ~ k ~ "=" ~ v; 111 } 112 113 return uriQueryString; 114 } 115 116 void addGroup(string group, string method, string value) 117 { 118 RouteGroup routeGroup = ("domain" == method) ? _domainGroups.get(group, null) : _directoryGroups.get(group, null); 119 120 if (routeGroup is null) 121 { 122 routeGroup = new RouteGroup(group); 123 124 _groups[group] = routeGroup; 125 126 if ("domain" == method) 127 { 128 _domainGroups[value] = routeGroup; 129 } 130 else 131 { 132 _directoryGroups[value] = routeGroup; 133 } 134 135 this._supportMultipleGroup = true; 136 } 137 } 138 139 RouteGroup getGroup(string group = DEFAULT_ROUTE_GROUP) 140 { 141 if (false == this._supportMultipleGroup) 142 { 143 return this._defaultGroup; 144 } 145 146 RouteGroup routeGroup = this._groups.get(group, null); 147 148 if (routeGroup is null) 149 { 150 return null; 151 } 152 153 return routeGroup; 154 } 155 156 void loadConfig() 157 { 158 this.loadConfig(DEFAULT_ROUTE_GROUP); 159 160 if (!this._supportMultipleGroup) 161 { 162 logDebug("Router multiple route group is disabled!"); 163 164 return; 165 } 166 else 167 { 168 logDebug("Router multiple route group is enabled.."); 169 } 170 171 // load this group routes from config file 172 foreach (key, obj; this._groups) 173 { 174 this.loadConfig(key); 175 } 176 } 177 178 void setSupportMultipleGroup(bool enabled = true) 179 { 180 this._supportMultipleGroup = enabled; 181 } 182 183 Router addRoute(string method, string path, HandleFunction handle, string group = DEFAULT_ROUTE_GROUP) 184 { 185 this.addRoute(this.makeRoute!HandleFunction(method, path, handle, group)); 186 187 return this; 188 } 189 190 Router addRoute(Route route, string group = DEFAULT_ROUTE_GROUP) 191 { 192 if (group == DEFAULT_ROUTE_GROUP) 193 { 194 this._defaultGroup.addRoute(route); 195 196 return this; 197 } 198 199 RouteGroup routeGroup = this._groups.get(group,null); 200 if (!routeGroup) 201 { 202 routeGroup = new RouteGroup(group); 203 204 this._groups[group] = routeGroup; 205 } 206 207 routeGroup.addRoute(route); 208 209 return this; 210 } 211 212 Route match(string domain, string method, string path) 213 { 214 path = this.mendPath(path); 215 216 if (false == this._supportMultipleGroup) 217 { 218 // don't support multiple route group, use defualt group match function 219 return this._defaultGroup.match(method,path); 220 } 221 222 RouteGroup routeGroup; 223 224 routeGroup = this.getGroupByDomain(domain); 225 226 if (!routeGroup) 227 { 228 if (path.length > 1) 229 { 230 import std.array; 231 // TODO: this is bug 232 string directory = split(path, "/")[1]; 233 234 235 routeGroup = this.getGroupByDirectory(directory); 236 if (routeGroup) 237 { 238 path = path[directory.length+1 .. $]; 239 } 240 else 241 { 242 routeGroup = this._defaultGroup; 243 } 244 } 245 else 246 { 247 routeGroup = this._defaultGroup; 248 } 249 } 250 251 return routeGroup.match(method,path); 252 } 253 254 string mendPath(string path) 255 { 256 if (path != "/") 257 { 258 import std.algorithm.mutation : strip; 259 260 return "/" ~ path.strip('/') ~ "/"; 261 } 262 263 return path; 264 } 265 } 266 267 private 268 { 269 // 270 void loadConfig(string group = DEFAULT_ROUTE_GROUP) 271 { 272 RouteGroup routeGroup; 273 274 logDebugf("load config for %s", group); 275 276 if (group == DEFAULT_ROUTE_GROUP) 277 { 278 routeGroup = this._defaultGroup; 279 } 280 else 281 { 282 routeGroup = this._groups.get(group, null); 283 if (routeGroup is null) 284 { 285 logWarningf("Group [%s] non-existent.", group); 286 return; 287 } 288 } 289 290 string configFile = (DEFAULT_ROUTE_GROUP == group) ? this._configPath ~ "routes" : this._configPath ~ group ~ ".routes"; 291 if(!exists(configFile))return; 292 293 // read file content 294 RouteConfig config; 295 RouteItem[] items = config.loadConfig(configFile); 296 297 Route route; 298 299 foreach (item; items) 300 { 301 route = this.makeRoute(item.methods, item.path, item.route, group); 302 if (route) 303 { 304 routeGroup.addRoute(route); 305 } 306 } 307 } 308 309 RouteGroup getGroupByDomain(string domain) 310 { 311 return this._domainGroups.get(domain, null); 312 } 313 314 RouteGroup getGroupByDirectory(string directory) 315 { 316 return this._directoryGroups.get(directory, null); 317 } 318 319 Route makeRoute(T = string)(string methods, string path, T mca, string group = DEFAULT_ROUTE_GROUP) 320 { 321 logDebug(methods,path,mca,group); 322 auto route = new Route(); 323 324 import std.string : toUpper; 325 326 methods = toUpper(methods); 327 328 path = this.mendPath(path); 329 330 route.path = path; 331 332 route.setGroup(group); 333 route.setPattern(path); 334 auto arr = split(methods,","); 335 HTTP_METHODS[] http_methods; 336 foreach(v;arr){ 337 http_methods ~= getMethod(v); 338 } 339 route.setMethods(http_methods); 340 341 static if (is (T == string)) 342 { 343 route.setRoute(mca); 344 345 import std.algorithm; 346 import std.string; 347 348 if (mca.startsWith("staticDir:")) 349 { 350 route.setModule("hunt.application.staticfile"); 351 route.setController("staticfile"); 352 route.setAction("doStaticFile"); 353 route.staticFilePath = mca.chompPrefix("staticDir:"); 354 } 355 else 356 { 357 string[] mcaArray = split(mca, "."); 358 359 if (mcaArray.length > 3 || mcaArray.length < 2) 360 { 361 logWarningf("this route config mca length is: %d (%s)", mcaArray.length, mca); 362 return null; 363 } 364 365 if (mcaArray.length == 2) 366 { 367 route.setController(mcaArray[0]); 368 route.setAction(mcaArray[1]); 369 } 370 else 371 { 372 route.setModule(mcaArray[0]); 373 route.setController(mcaArray[1]); 374 route.setAction(mcaArray[2]); 375 } 376 377 import std.regex; 378 import std.array; 379 380 auto matches = path.matchAll(regex(`:(\w+)`)); 381 if (matches) 382 { 383 string[int] paramKeys; 384 int paramCount = 0; 385 string pattern = path; 386 string urlTemplate = path; 387 388 foreach (m; matches) 389 { 390 paramKeys[paramCount] = m[1]; 391 pattern = pattern.replaceFirst(m[0], "([^/]*)"); 392 urlTemplate = urlTemplate.replaceFirst(m[0], "{" ~ m[1] ~ "}"); 393 paramCount++; 394 } 395 396 route.setPattern(pattern); 397 route.setParamKeys(paramKeys); 398 route.setRegular(true); 399 route.setUrlTemplate(urlTemplate); 400 } 401 } 402 403 string handleKey = this.makeRequestHandleKey(route); 404 405 route.handle = getRouteFormList(handleKey); 406 } 407 else 408 { 409 route.handle = mca; 410 } 411 412 if (route.handle is null) 413 { 414 logDebugf("handle is null (%s).", route.getPattern()); 415 return null; 416 } 417 418 return route; 419 } 420 421 string makeRequestHandleKey(Route route) 422 { 423 string handleKey; 424 425 if (route.staticFilePath == string.init) 426 { 427 if (route.getModule() == null) 428 { 429 handleKey = "app.controller." ~ ((route.getGroup() == DEFAULT_ROUTE_GROUP) ? "" : route.getGroup() ~ ".") ~ route.getController() ~ "." ~ route.getController() ~ "controller." ~ route.getAction(); 430 } 431 else 432 { 433 handleKey = "app." ~ route.getModule() ~ ".controller." ~ ((route.getGroup() == DEFAULT_ROUTE_GROUP) ? "" : route.getGroup() ~ ".") ~ route.getController() ~ "." ~ route.getController() ~ "controller." ~ route.getAction(); 434 } 435 } 436 else 437 { 438 handleKey = "hunt.application.staticfile.StaticfileController.doStaticFile"; 439 } 440 441 import std.string : toLower; 442 443 return handleKey.toLower(); 444 } 445 } 446 447 private 448 { 449 RouteGroup _defaultGroup; 450 451 RouteGroup[string] _directoryGroups; 452 RouteGroup[string] _domainGroups; 453 RouteGroup[string] _groups; 454 455 // enable muiltple route group 456 bool _supportMultipleGroup = false; 457 458 import hunt.init; 459 alias _configPath = DEFAULT_CONFIG_PATH; 460 } 461 }